閱讀本篇文章前,仔細想想看
- 如何使用類別的繼承(Inheritance)?
- 為何我們設計類別的成員時,會儘量以
private
模式為基準?什麼時候該開放成員給外部使用呢?private
與protected
模式間的最大差別在哪呢?- 通常子類別建構物件時,要如何連結父類別初始化物件的邏輯呢?
- 使用
super
有沒有特別需要注意的事項呢?如果還沒理解完畢的話,可以先翻看前一篇文章喔!
今天講的東西不會像昨天討論類別繼承一樣 —— 篇幅太大。(筆者也被折騰了一番)
正文開始!
首先,有些人可能對靜態(Static)與動態(Dynamic)這兩個詞在類別的概念會搞混。不過大部分在講 OOP 時我們不會刻意提到動態屬性與方法,那是因為我們早就在使用了。
其實最簡單的想法是這樣:
類別新增物件時的成員變數與成員方法 —— 皆可被當成動態屬性與方法
所以那些類別成員們(Class Members)都是被形容成動態的概念,筆者是這麼看待的。
那為何還要分動靜態?靜態又是什麼概念?
假設想要寫一個類別是專門計算幾何圖形中 —— 跟圓形有關的類別,以下簡單的把程式碼寫出來進行示範。
以上的程式碼的說明如下。
CircleGeometry
的成員變數事實上有兩個(不要看到建構子函式裡面只有一個參數就認定只有一個成員變數喔!):
PI
被設為 private
模式代表圓周率 3.14radius
代表圓的半徑,並且需要經過建構子進行初始化的動作而 CircleGeometry
的成員方法也有兩個:
area
代表計算圓的面積,其型別為 (): number
circumference
代表計算圓的周長,其型別也是 (): number
這邊筆者認為讀者可以自行試試看如何使用這個類別建立物件並且呼叫方法,所以以下的程式碼暫且就不測了。
好的!上述的 CircleGeometry
的成員們為何會被認定並非靜態的主要原因是:在建構物件的時候,我們的成員變數的值會不定。
譬如可以建立半徑為 2
的圓 —— new CircleGeometry(2)
;也可以建構半徑為 2.71828
的圓 —— new CircleGeometry(2.71828)
以及更多無限種案例。每一次建立半徑不同的圓,物件的成員方法 area
與 circumference
也會計算出不同的結果。
儘管類別藍圖提供的成員們都是一樣的格式,但是每一次建構出的新物件 —— 該物件的值以及呼叫方法出現的結果可能也會變得不同,因此讓人感覺到物件的狀態是動態的。
如果想要了解相對於物件的狀態是動態的意義,必須把焦點轉換為:把類別本身看待成一個物件 —— 類別本身的屬性與方法,這些東西就被稱作是靜態屬性與方法。
筆者換個角度來解釋靜態屬性與方法。
讀者應該看過 JavaScript 裡面的 Math
這個物件吧 ~ 但它跟普通類別的使用方式不同 —— 它不需要用 new
去建構物件然後呼叫方法,而是直接提供 Math
本身的屬性與方法讓開發者使用。
我們可以稱 Math.PI
中的 PI
為 Math
這個類別的靜態屬性(Static Property);相對地,那些 Math.random
、Math.sin
、Math.pow
與更多從 Math
延伸出來的方法都統稱為 Math
這個類別的靜態方法(Static Methods)。
重點 1. 類別的靜態屬性與方法
不需要經由建構物件的過程,而是直接從類別本身提供的屬性與方法,皆稱之為靜態屬性與方法,又被稱為靜態成員(Static Members)。
因此,靜態成員具有一個很重要的特點:不管物件被建立多少次,靜態成員只會有一個版本 —— 這也符合靜態的概念:固定、單一版本、不變的原則等等。
通常會使用靜態成員的狀況:
- 靜態成員不會隨著物件建構的不同而隨之改變
- 靜態成員可以作為類別本身提供的工具,不需要經過建構物件的程序;換句話說:類別提供之靜態成員本身就是可被操作的介面
static
想要仿造 Math
類別 —— 我們可以使用 static
關鍵字將 CircleGeometry
裡的成員轉換成靜態成員們。
要注意的是:由於 PI
從成員變成了靜態成員,因此不能使用 this.PI
,而是 StaticCircleGeometry.PI
來取用。
那是因為:靜態成員是綁在類別身上,並非類別建立起來的物件 —— 因為不需要經過 new
建立物件的過程就可以從類別身上取用!
再來,因為捨棄了原本建構新物件時,必須提供 radius
作為物件的成員變數這個管道 —— 沒了物件本身建構後的 radius
屬性,所以必須改寫 area
與 circumference
這兩個方法:將型別從原本的 (): number
變成 (radius: number): number
,並且從 public
成員方法變成靜態方法。
以下比對 CircleGeometry
跟 StaticCircleGeometry
之間的使用差別。(編譯過後並且使用 node
執行結果如圖一)
圖一:結果都ㄧ樣,但實踐的過程不ㄧ樣罷了
重點 2. 宣告與使用靜態屬性與方法 Static Properties & Methods
若想要在某類別
C
宣告的靜態屬性Pstatic
與方法Mstatic
,程式碼格式如下:若想使用類別
C
的靜態屬性Pstatic
與方法Mstatic
,則可以直接從C
呼叫:C.Pstatic
與C.Mstatic(/* 參數... */)
static
與存取修飾子 Access Modifiers事實上,靜態成員跟普通類別成員有類似的地方 —— 都是可以設定存取模式的!
假設想要防止使用者竄改 StaticCircleGeometry
裡面的靜態屬性 PI
之值,並且提供另一個管道跟讓使用者得知它的值為多少,我們可以這麼做:
讀者可以發現 private static
的概念很簡單:private
模式可以封裝我們的屬性與方法,加在 static
旁邊就只是告訴開發者不能亂動這個靜態成員。因此類別裡面是可以使用靜態屬性 —— 因此 area
與 circumference
這兩個方法裡面就算去呼叫 StaticCircleGeometry
是不會出現錯誤的,但是如果你在外面呼叫就會被 TypeScript 警告喔!(被 TypeScript 查驗結果如圖二;錯誤訊息如圖三)
圖二:強行取得 private
模式下的靜態屬性或方法,也是會被記警告的!
圖三:TypeScript 很明確就跟你講了,屬性 PI
為 private
模式,只能在類別內部取用呢!
靜態成員這個功能可是非常實用的!
重點 3. 靜態成員與存取修飾子 Static Members & Access Modifiers
類別的靜態成員可以被設定不同的存取模式 —— 包含
public
、private
以及protected
模式。其運作的方式跟普通成員變數與方法的流程一模一樣;差別就在於 —— 普通成員是綁定在建構過後的物件上,而靜態成員則是跟類別本身綁定。
還記得上一篇的範例嗎?筆者短暫把必要的程式碼部分截取下來:
事實上,我們必須訂立站點與站點間的資訊 stationsDetail
與站點的列表 stops
。不管車票 TrainTicket
被建立多少次,但站點資訊是不太可能隨之變動的!
因此可以將這兩個成員設定成靜態成員。
不過把成員從普通的狀態變成類別的靜態成員過程中,必須要把所有跟靜態成員相關的程式碼,原本是用 this
去呼叫就得改成用類別 TrainTicket
去呼叫喔!
因此裡面的 isStopExist
函式取用到 stops
這個屬性就必須被改成靜態屬性的呼叫方式:
而 deriveDuration
則是因為有取用到 stationsDetail
屬性,因此也必須要被轉換:
讀者試試看
筆者剛剛展示過:將
TrainTicket
裡的stops
與stationsDetail
更改為靜態屬性。讀者可以自行編譯並且驗證程式碼的運行。另外,除了可將
stops
與stationsDetail
改成靜態屬性外,讀者也可以確認看看,將這兩種屬性從private
轉成public
並且測看看能不能在外面呼叫。而且如果這些屬性可以在外面呼叫的話,讀者甚至可以自訂車票的站點表來測測看不同的結果 —— 體會一下靜態成員的語法運作過程喔。當然,創意一點的方式是:能不能夠寫簡單的靜態方法,進行站點更改或新增站點的工作?這些就可以留給讀者發想~
今天的主題應該比昨天簡單很多,畢竟繼承的概念本來就多到炸筆者也沒想到會寫到篇幅太長。
下一篇也很簡單,筆者要介紹存取方法 Access Methods~
不過筆者照樣要提醒,剛入門 OOP 的讀者們,會建議來回複習類別的基礎,因為後面還有一大堆還沒講完。目前已經提到的有:
super
記得,下一篇要講到的 Access Methods 與之前講完的 Access Modifiers,儘管都有 Access 這個字,可是意義天差地遠啊~可不要搞混了。
下圖中的類別成員前面的存取修飾子和static的位置寫反了, 正確順序應該是先private再static
因此 area 與 circumference 這兩個方法裡面就算去呼叫 StaticCircleGeometry 是不會出現錯誤的,但是如果你在外面呼叫就會被 TypeScript 警告喔!(被 TypeScript 查驗結果如圖二;錯誤訊息如圖三)
嗨~ 這邊好像漏字了?
是不是:StaticCircleGeometry
--> StaticCircleGeometry.PI
呢?